package com.atgouwu.mall.seckill.service.impl;

import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.atgouwu.mall.common.mq.SeckillOrderTo;
import com.atgouwu.mall.common.utils.R;
import com.atgouwu.mall.common.vo.MemberResponseVo;
import com.atgouwu.mall.seckill.feign.CouponFeignService;
import com.atgouwu.mall.seckill.feign.ProductFeignService;
import com.atgouwu.mall.seckill.interceptor.MyHandlerInterceptor;
import com.atgouwu.mall.seckill.service.SeckillService;
import com.atgouwu.mall.seckill.to.SeckillSkuRedisTo;
import com.atgouwu.mall.seckill.vo.SeckillSessionWithSkusVo;
import com.atgouwu.mall.seckill.vo.SkuInfoVo;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RSemaphore;
import org.redisson.api.RedissonClient;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

@Slf4j
@Service
public class SeckillServiceImpl implements SeckillService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private CouponFeignService couponFeignService;

    @Autowired
    private ProductFeignService productFeignService;

    @Autowired
    private RedissonClient redissonClient;

    @Autowired
    private RabbitTemplate rabbitTemplate;

    private final String SESSION__CACHE_PREFIX = "seckill:sessions:";

    private final String SECKILL_CHARE_PREFIX = "seckill:skus";

    private final String SKU_STOCK_SEMAPHORE = "seckill:stock:";    //+商品随机码

    //上架最近三天需要秒杀商品到redis
    @Override
    public void uploadSeckillSkuLatest3Days() {
        R lates3DaySession = couponFeignService.getLates3DaySession(); //获取最近三天需参加秒杀商品(远程调用coupon优惠卷服务)
        if (lates3DaySession.getCode() == 200) {
            List<SeckillSessionWithSkusVo> sessionData = lates3DaySession.getData("data", new TypeReference<List<SeckillSessionWithSkusVo>>() {
            });
            saveSessionInfos(sessionData); //缓存秒杀活动
            saveSessionSkuInfo(sessionData); //缓存关联商品
        }
    }

    //缓存秒杀活动
    private void saveSessionInfos(List<SeckillSessionWithSkusVo> sessions) {
        sessions.stream().forEach(session -> {
            long startTime = session.getStartTime().getTime(); //获取当前活动的开始和结束时间的时间戳
            long endTime = session.getEndTime().getTime();
            String key = SESSION__CACHE_PREFIX + startTime + "_" + endTime;
            Boolean hasKey = redisTemplate.hasKey(key); //判断Redis中是否有该信息,没有才进行添加
            if (!hasKey) {
                List<String> skuIds = session.getRelationSkus().stream().map(item -> item.getPromotionSessionId() + "-" + item.getSkuId().toString()).collect(Collectors.toList()); //获取活动中所有商品skuId
                redisTemplate.opsForList().leftPushAll(key, skuIds); //存入redis
            }
        });
    }

    //缓存关联商品
    private void saveSessionSkuInfo(List<SeckillSessionWithSkusVo> sessions) {
        sessions.stream().forEach(session -> {
            BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX); //准备hash操作,绑定hash
            session.getRelationSkus().stream().forEach(seckillSkuVo -> {
                String token = UUID.randomUUID().toString().replace("-", ""); //生成随机码(防止恶意攻击,活动开始时才暴露)
                String redisKey = seckillSkuVo.getPromotionSessionId().toString() + "-" + seckillSkuVo.getSkuId().toString();
                if (!operations.hasKey(redisKey)) {
                    SeckillSkuRedisTo redisTo = new SeckillSkuRedisTo();
                    Long skuId = seckillSkuVo.getSkuId();
                    R info = productFeignService.getSkuInfo(skuId); //获取商品(远程调用product商品服务)
                    if (info.getCode() == 200) {
                        SkuInfoVo skuInfo = info.getData("data", new TypeReference<SkuInfoVo>() {
                        });
                        redisTo.setSkuInfo(skuInfo); //商品信息
                    }
                    BeanUtils.copyProperties(seckillSkuVo, redisTo); //sku秒杀信息
                    redisTo.setStartTime(session.getStartTime().getTime()); //秒杀时间
                    redisTo.setEndTime(session.getEndTime().getTime());
                    redisTo.setRandomCode(token); //随机码(防止恶意攻击)
                    String seckillValue = JSON.toJSONString(redisTo); //序列化json格式存入Redis
                    operations.put(redisKey, seckillValue);
                    RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token); //当前场次商品库存信息已上架就不需上架,用库存作为分布式Redisson信号量(限流)
                    semaphore.trySetPermits(seckillSkuVo.getSeckillCount()); //redisson信号量(高并发,商品可秒杀数量作为信号量,有数量才能操作数据库)
                }
            });
        });
    }

    //获取当前可参加秒杀商品
    @SentinelResource(value = "getCurrentSeckillSkusResource", blockHandler = "blockHandler")
    @Override
    public List<SeckillSkuRedisTo> getCurrentSeckillSkus() {
        try (Entry entry = SphU.entry("seckillSkus")) {
            long currentTime = System.currentTimeMillis(); //当前时间
            Set<String> keys = redisTemplate.keys(SESSION__CACHE_PREFIX + "*"); //从Redis查所有key以seckill:sessions开头数据
            for (String key : keys) {
                String replace = key.replace(SESSION__CACHE_PREFIX, ""); //seckill:sessions:1594396764000_1594453242000
                String[] s = replace.split("_");
                long startTime = Long.parseLong(s[0]); //获取存入Redis商品开始时间
                long endTime = Long.parseLong(s[1]); //获取存入Redis商品结束时间
                if (currentTime >= startTime && currentTime <= endTime) { //判断是否是当前秒杀场次
                    List<String> range = redisTemplate.opsForList().range(key, -100, 100);
                    BoundHashOperations<String, String, String> hasOps = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
                    assert range != null;
                    List<String> listValue = hasOps.multiGet(range); //获取这个秒杀场次需要的所有商品信息
                    if (!CollectionUtils.isEmpty(listValue)) {
                        List<SeckillSkuRedisTo> collect = listValue.stream().map(item -> {
                            String items = (String) item;
                            SeckillSkuRedisTo redisTo = JSON.parseObject(items, SeckillSkuRedisTo.class);
                            //redisTo.setRandomCode(null); //秒杀开始返回随机码,预告未开始不返回
                            return redisTo;
                        }).collect(Collectors.toList());
                        return collect;
                    }
                    break;
                }
            }
        } catch (BlockException e) {
            log.error("资源被限流{}", e.getMessage());
        }
        return null;
    }

    public List<SeckillSkuRedisTo> blockHandler(BlockException e) {
        log.error("getCurrentSeckillSkusResource被限流了,{}", e.getMessage());
        return null;
    }

    //根据skuId查询商品是否参加秒杀活动
    @Override
    public SeckillSkuRedisTo getSkuSeckilInfo(Long skuId) {
        BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX); //从redis获取所有需要秒杀商品的key: seckill:skus
        Set<String> keys = hashOps.keys();
        if (!CollectionUtils.isEmpty(keys)) {
            String reg = "\\d-" + skuId; //4-45正则表达式进行匹配
            for (String key : keys) {
                if (Pattern.matches(reg, key)) {
                    String redisValue = hashOps.get(key);
                    SeckillSkuRedisTo redisTo = JSON.parseObject(redisValue, SeckillSkuRedisTo.class);
                    Long currentTime = System.currentTimeMillis();
                    Long startTime = redisTo.getStartTime();
                    Long endTime = redisTo.getEndTime();
                    if (!(currentTime >= startTime && currentTime <= endTime)) { //当前时间大于等于秒杀活动开始时间且小于活动结束时间
                        redisTo.setRandomCode(null); //商品详情页预告时不返回随机码
                    }
                    return redisTo;
                }
            }
        }
        return null;
    }

    //立即抢购
    @Override
    public String kill(String killId, String key, Integer num) throws InterruptedException {
        long s1 = System.currentTimeMillis();
        MemberResponseVo user = MyHandlerInterceptor.loginUser.get(); //获取当前用户
        BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX); //从redis获取当前秒杀商品
        String skuInfoValue = hashOps.get(killId);
        if (StringUtils.isEmpty(skuInfoValue)) {
            return null;
        }
        SeckillSkuRedisTo redisTo = JSON.parseObject(skuInfoValue, SeckillSkuRedisTo.class);
        Long startTime = redisTo.getStartTime();
        Long endTime = redisTo.getEndTime();
        long currentTime = System.currentTimeMillis();
        if (currentTime >= startTime && currentTime <= endTime) { //判断当前秒杀是否在活动时间内(效验时间合法性)
            String randomCode = redisTo.getRandomCode();
            String skuId = redisTo.getPromotionSessionId() + "-" + redisTo.getSkuId();
            if (randomCode.equals(key) && killId.equals(skuId)) { //效验随机码和商品id
                Integer seckillLimit = redisTo.getSeckillLimit();
                String seckillCount = redisTemplate.opsForValue().get(SKU_STOCK_SEMAPHORE + randomCode); //信号量
                Integer count = Integer.valueOf(seckillCount);
                if (count > 0 && count > num && num <= seckillLimit) { //信号量大于0,信号量大于购买数量,购买数量小于库存
                    String redisKey = user.getId() + "-" + skuId; //SETNX原子性处理
                    Long ttl = endTime - currentTime; //设置自动过期(活动结束时间-当前时间)
                    Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS); //占位,验证这个人是否买过(幂等性),如果秒杀成功就去占位,userId-sessionId-skuId
                    if (aBoolean) {
                        RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode); //信号量
                        boolean semaphoreCount = semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS); //信号量减去购买数量
                        if (semaphoreCount) {
                            String timeId = IdWorker.getTimeId(); //订单号
                            SeckillOrderTo orderTo = new SeckillOrderTo();
                            orderTo.setOrderSn(timeId);
                            orderTo.setMemberId(user.getId());
                            orderTo.setNum(num);
                            orderTo.setPromotionSessionId(redisTo.getPromotionSessionId());
                            orderTo.setSkuId(redisTo.getSkuId());
                            orderTo.setSeckillPrice(redisTo.getSeckillPrice());
                            rabbitTemplate.convertAndSend("order-event-exchange", "order.seckill.order", orderTo); //订单信息发送给MQ
                            long s2 = System.currentTimeMillis();
                            log.info("耗时..." + (s2 - s1));
                            return timeId;
                        }
                    }
                }
            }
        }
        long s3 = System.currentTimeMillis();
        log.info("耗时..." + (s3 - s1));
        return null;
    }

}
